package cn.lingyangwl.agile.auth.oauth2.support;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.lingyangwl.agile.auth.AuthApi;
import cn.lingyangwl.agile.auth.constants.CaptchaKeys;
import cn.lingyangwl.agile.auth.model.entity.LocalUser;
import cn.lingyangwl.agile.auth.oauth2.Authentication;
import cn.lingyangwl.agile.auth.oauth2.OAuth2TokenContext;
import cn.lingyangwl.agile.auth.oauth2.model.OAuth2TokenResp;
import cn.lingyangwl.agile.auth.oauth2.model.RegisteredClient;
import cn.lingyangwl.agile.auth.service.CaptchaService;
import cn.lingyangwl.agile.auth.service.OAuth2ClientService;
import cn.lingyangwl.agile.auth.service.OAuth2TokenService;
import cn.lingyangwl.agile.auth.utils.OAuth2Utils;
import cn.lingyangwl.agile.model.constants.AppType;
import cn.lingyangwl.agile.model.constants.OAuth2Cons;
import cn.lingyangwl.agile.model.constants.RequestCons;
import cn.lingyangwl.agile.model.enums.AuthErrorEnum;
import cn.lingyangwl.agile.model.module.auth.*;
import cn.lingyangwl.agile.model.module.tenant.TenantContext;
import cn.lingyangwl.framework.tool.core.StringUtils;
import cn.lingyangwl.framework.tool.core.exception.Assert;
import cn.lingyangwl.framework.tool.core.exception.BizException;
import com.github.yitter.idgen.YitIdHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.MultiValueMap;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author shenguangyang
 */
@Slf4j
public abstract class BaseOauth2Grant {
    private final static Map<GrantTypeEnum, BaseOauth2Grant> oauth2GrantMap = new ConcurrentHashMap<>();

    @Resource
    protected AuthApi authApi;
    @Resource
    protected CaptchaService captchaService;
    @Resource
    protected OAuth2TokenService tokenService;
    @Resource
    protected OAuth2ClientService clientService;
    @Resource
    protected RedisTemplate<String, Object> redisTemplate;
    @Resource
    protected AuthHandler authHandler;

    @PostConstruct
    public void initBaseOauth2Grant() {
        oauth2GrantMap.put(grantType(), this);
    }

    /**
     * 执行授权逻辑
     */
    protected abstract LoginUser authenticate(Authentication authentication);

    /**
     * 自定义token参数 TODO 待定
     */
    protected void customizeToken() {

    }

    /**
     * 获取授权类型
     */
    protected abstract GrantTypeEnum grantType();

    /**
     * 检查客户端
     */
    protected abstract void checkClient(RegisteredClient registeredClient);

    /**
     * 是否检查验证码
     */
    protected abstract boolean isCheckCaptcha();


    /**
     * 校验参数
     * @param request 请求
     */
    public void checkParams(HttpServletRequest request) {
        String tenantId = request.getHeader(RequestCons.Headers.TENANT_ID);
        Assert.notEmpty(tenantId, "租户ID不能为空");
    }

    public static OAuth2TokenResp grant(GrantTypeEnum grantTypeEnum, HttpServletRequest request) {
        BaseOauth2Grant oauth2Grant = oauth2GrantMap.get(grantTypeEnum);
        if (Objects.isNull(oauth2Grant)) {
            throw new BizException("尚未失效 {} 授予类型", grantTypeEnum.getCode());
        }
        return oauth2Grant.doGrant(request);
    }

    private String[] obtainBasicAuthorization(HttpServletRequest request) {
        String[] clientIdAndSecret = OAuth2Utils.obtainBasicAuthorization(request);
        if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) {
            throw new BizException("client_id 或 client_secret 未正确传递");
        }
        return clientIdAndSecret;
    }

    /**
     * TODO 限制授权次数
     */
    protected OAuth2TokenResp doGrant(HttpServletRequest request) {
        MultiValueMap<String, String> parameters = OAuth2Utils.getParameters(request);
        // TODO 查询客户端时候会得到租户id
        TenantContext.get().setTenantId(request.getHeader(RequestCons.Headers.TENANT_ID));

        // 生成一个登录id
        String loginId = String.valueOf(YitIdHelper.nextId());

        // 获取请求头令牌
        String[] clientIdAndSecret = obtainBasicAuthorization(request);
        String clientId = clientIdAndSecret[0];
        String clientSecret = clientIdAndSecret[1];

        // grant_type
        String grantType = request.getParameter(OAuth2ParamKeys.GRANT_TYPE);
        String redirectUri = request.getParameter(OAuth2ParamKeys.REDIRECT_URI);
        if (!grantType().getCode().equals(grantType)) {
            log.warn("oauth2 grantType [{}] is not supported", grantType);
            throw new BizException(AuthErrorEnum.OAUTH2_UNAUTHORIZED_CLIENT);
        }

        // 校验授权返回
        String scopeStr = parameters.getFirst(OAuth2ParamKeys.SCOPE);
        if (StringUtils.isEmpty(scopeStr)) {
            throw new BizException(AuthErrorEnum.SCOPE_IS_EMPTY);
        }

        String appTypeStr = request.getHeader(RequestCons.Headers.SYSTEM_TYPE);
        AppType appType = AppType.getByValue(appTypeStr);
        if (appType == null) {
            log.warn("用户登录的应用类型未知");
        }

        List<String> scopes = OAuth2Utils.buildScopes(scopeStr).stream().distinct().collect(Collectors.toList());

        // 校验个性化参数
        checkParams(request);

        // 扩展信息
        Map<String, Object> additionalParameters = parameters.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));

//        String systemTypeCode = Optional.ofNullable(systemType).map(UserCons.SystemType::getCode).orElse("");

        // 获取客户端
        RegisteredClient client = clientService.validOAuthClient(clientId, clientSecret, grantType, scopes, redirectUri);

        // 检查客户端
        checkClient(client);

        Authentication authentication = Authentication.builder()
                .loginId(loginId)
                .additionalParameters(additionalParameters)
                .client(client).build();
        try {

            // 检查验证码
            checkCaptcha(client, additionalParameters);

            // 执行用户授权
            LoginUser authUser = authenticate(authentication);
            if (Objects.isNull(authUser)) {
                throw new BizException("授权失败");
            }
            authUser.setLoginId(loginId);
            authUser.setClientId(clientId);
            authUser.setLoginTime(System.currentTimeMillis());

            // 设置Jwt公共的存储信息
            Map<String, Object> claims = authentication.getClaims();
            claims.put(TokenClaimsKeys.LOGIN_ID, loginId);
            claims.put(TokenClaimsKeys.OPEN_ID, authUser.getOpenId());
            claims.put(TokenClaimsKeys.USER_ID, authUser.getUserId());
            claims.put(TokenClaimsKeys.USER_NAME, authUser.getUserName());
            claims.put(TokenClaimsKeys.GRANT_TYPE, grantType);
            claims.put(TokenClaimsKeys.CLIENT_ID, clientId);
            claims.put(TokenClaimsKeys.SCOPES, StringUtils.toStr(new ArrayList<>(scopes), ","));

            // 生成并保存token
            OAuth2TokenContext tokenContext = OAuth2TokenContext.builder()
                    .authorizedScopes(scopes).registeredClient(client)
                    .authUser(authUser).claims(claims)
                    .grantType(grantType).loginId(loginId).build();

            // 保存用户基本信息到缓存中
            this.saveUserToCache(authentication, authUser);

            OAuth2TokenResp tokenResp = tokenService.issueToken(tokenContext);

            // 处理授权成功事件
            authHandler.onSuccess(tokenContext.getRegisteredClient(), tokenContext.getAuthUser());
            return tokenResp;
        } catch (Exception e) {
            if (e instanceof BizException) {
                // 处理授权失败事件
                String username = request.getParameter(OAuth2ParamKeys.USERNAME);
                authHandler.onFailure(username, authentication, e);
            }
            throw e;
        }
    }


    public void saveUserToCache(Authentication authentication, LoginUser authUser) {
        Duration accessTokenValidity = authentication.getClient().getAccessTokenTimeToLive();

        // 保存用户基本信息
        String cacheKey = String.format(OAuth2Cons.CACHE_USER_DETAILS, authUser.getUserId(), authentication.getLoginId());
        log.info("user details cache key: {}", cacheKey);
        redisTemplate.opsForValue().set(cacheKey, authUser, accessTokenValidity.getSeconds(), TimeUnit.SECONDS);
    }

    private void checkCaptcha(RegisteredClient client, Map<String, Object> reqParameters) {
        // 检查验证码
        if (isCheckCaptcha() && client.getUseCaptcha()) {
            String code = ObjectUtil.defaultIfNull(reqParameters.get(CaptchaKeys.CODE), "") + "";
            String uuid = ObjectUtil.defaultIfNull(reqParameters.get(CaptchaKeys.UUID), "") + "";
            if (StringUtils.isEmpty(code) || StringUtils.isEmpty(uuid)) {
                throw new BizException(AuthErrorEnum.CAPTCHA_IS_EMPTY);
            }

            if (!captchaService.checkCaptcha(code, uuid)) {
                throw new BizException(AuthErrorEnum.CAPTCHA_ERROR);
            }

            // 验证成功则删除验证码
            captchaService.deleteCaptcha(uuid);
        }
    }


    protected LoginUser buildAuthUser(String account, LocalUser localUser) {
        LoginUser loginUser = new LoginUser();
        // 查询账号
        UserAccount userAccount = UserAccount.builder()
                .userName(localUser.getUserName()).email(localUser.getEmail())
                .phone(localUser.getPhone()).password(localUser.getPassword()).status(localUser.getStatus())
                .build();

        loginUser.setUserId(localUser.getUserId())
                .setUserName(account).setAccount(userAccount);
        return loginUser;
    }
}
